/** 
 * Allows the eventClass to be set in MXML, e.g.
 * <natives:NativeSignal id="clicked" eventType="click" target="{this}">{MouseEvent}</natives:NativeSignal>
 */
package org.osflash.signals.natives;

import org.osflash.signals.ISlot;
import org.osflash.signals.Slot;
import org.osflash.signals.SlotList;
import flash.errors.ArgumentError;
import flash.errors.IllegalOperationError;
import flash.events.Event;
import flash.events.IEventDispatcher;

class NativeSignal implements INativeDispatcher {
	public var eventType(getEventType, setEventType) : String;
	public var eventClass(getEventClass, setEventClass) : Class<Dynamic>;
	public var valueClasses(getValueClasses, setValueClasses) : Array<Dynamic>;
	public var numListeners(getNumListeners, never) : UInt;
	public var target(getTarget, setTarget) : IEventDispatcher;

	var _target : IEventDispatcher;
	var _eventType : String;
	var _eventClass : Class<Dynamic>;
	var _valueClasses : Array<Dynamic>;
	var slots : SlotList;
	/**
	 * Creates a NativeSignal instance to dispatch events on behalf of a target object.
	 * @param	target The object on whose behalf the signal is dispatching events.
	 * @param	eventType The type of Event permitted to be dispatched from this signal. Corresponds to Event.type.
	 * @param	eventClass An optional class reference that enables an event type check in dispatch(). Defaults to flash.events.Event if omitted.
	 */
	public function new(target : IEventDispatcher = null, eventType : String = "", eventClass : Class<Dynamic> = null) {
		slots = SlotList.NIL;
		this.target = target;
		this.eventType = eventType;
		this.eventClass = eventClass;
	}

	/** @inheritDoc */
	public function getEventType() : String {
		return _eventType;
	}

	public function setEventType(value : String) : String {
		_eventType = value;
		return value;
	}

	/** @inheritDoc */
	public function getEventClass() : Class<Dynamic> {
		return _eventClass;
	}

	public function setEventClass(value : Class<Dynamic>) : Class<Dynamic> {
		_eventClass = (value != null) ? value : Event;
		_valueClasses = [_eventClass];
		return value;
	}

	/** @inheritDoc */
	@:meta(ArrayElementType(name="Class"))
	public function getValueClasses() : Array<Dynamic> {
		return _valueClasses;
	}

	public function setValueClasses(value : Array<Dynamic>) : Array<Dynamic> {
		eventClass = (value != null && value.length > 0) ? value[0] : null;
		return value;
	}

	/** @inheritDoc */
	public function getNumListeners() : UInt {
		return slots.length;
	}

	/** @inheritDoc */
	public function getTarget() : IEventDispatcher {
		return _target;
	}

	public function setTarget(value : IEventDispatcher) : IEventDispatcher {
		if(value == _target) return value;
		if(_target != null) removeAll();
		_target = value;
		return value;
	}

	/**
	 * @inheritDoc
	 * @throws flash.errors.IllegalOperationError <code>IllegalOperationError</code>: You cannot addOnce() then add() the same listener without removing the relationship first.
	 * @throws ArgumentError <code>ArgumentError</code>: Given listener is <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 */
	public function add(listener : Dynamic) : ISlot {
		return addWithPriority(listener);
	}

	/**
	 * @inheritDoc
	 * @throws flash.errors.IllegalOperationError <code>IllegalOperationError</code>: You cannot addOnce() then add() the same listener without removing the relationship first.
	 * @throws ArgumentError <code>ArgumentError</code>: Given listener is <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 */
	public function addWithPriority(listener : Dynamic, priority : Int = 0) : ISlot {
		return registerListenerWithPriority(listener, false, priority);
	}

	/**
	 * @inheritDoc
	 * @throws flash.errors.IllegalOperationError <code>IllegalOperationError</code>: You cannot addOnce() then add() the same listener without removing the relationship first.
	 * @throws ArgumentError <code>ArgumentError</code>: Given listener is <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 */
	public function addOnce(listener : Dynamic) : ISlot {
		return addOnceWithPriority(listener);
	}

	/**
	 * @inheritDoc
	 * @throws flash.errors.IllegalOperationError <code>IllegalOperationError</code>: You cannot addOnce() then add() the same listener without removing the relationship first.
	 * @throws ArgumentError <code>ArgumentError</code>: Given listener is <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 */
	public function addOnceWithPriority(listener : Dynamic, priority : Int = 0) : ISlot {
		return registerListenerWithPriority(listener, true, priority);
	}

	/** @inheritDoc */
	public function remove(listener : Dynamic) : ISlot {
		var slot : ISlot = slots.find(listener);
		if(slot == null) return null;
		_target.removeEventListener(_eventType, slot.execute1);
		slots = slots.filterNot(listener);
		return slot;
	}

	/** @inheritDoc */
	public function removeAll() : Void {
		var slotsToProcess : SlotList = slots;
		while(slotsToProcess.nonEmpty) {
			target.removeEventListener(_eventType, slotsToProcess.head.execute1);
			slotsToProcess = slotsToProcess.tail;
		}

		slots = SlotList.NIL;
	}

	/**
	 * @inheritDoc
	 * @throws ArgumentError <code>ArgumentError</code>: Event object expected.
	 * @throws ArgumentError <code>ArgumentError</code>: No more than one Event object expected.
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Event object cannot be <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Event object [event] is not an instance of [eventClass].
	 * @throws ArgumentError <code>ArgumentError</code>: Event object has incorrect type. Expected [eventType] but was [event.type].
	 */
	public function dispatch(valueObjects:Array<Dynamic>=null) : Void {
		if(valueObjects == null)
			valueObjects = [];
		//TODO: check if ...valueObjects can ever be null.
		if(null == valueObjects) throw new ArgumentError("Event object expected.");
		if(valueObjects.length != 1) throw new ArgumentError("No more than one Event object expected.");
		var e : Event = try cast(valueObjects[0], Event) catch(e:Dynamic) null;
		dispatchEvent(e);
	}

	/**
	 * Unlike other signals, NativeSignal does not dispatch null
	 * because it causes an exception in EventDispatcher.
	 * @inheritDoc
	 * @throws ArgumentError <code>ArgumentError</code>: Target object cannot be <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Event object cannot be <code>null</code>.
	 * @throws ArgumentError <code>ArgumentError</code>: Event object [event] is not an instance of [eventClass].
	 * @throws ArgumentError <code>ArgumentError</code>: Event object has incorrect type. Expected [eventType] but was [event.type].
	 */
	public function dispatchEvent(event : Event) : Bool {
		if(target == null) throw new ArgumentError("Target object cannot be null.");
		if(event == null) throw new ArgumentError("Event object cannot be null.");
		if(!(Std.is(event, eventClass))) throw new ArgumentError("Event object " + event + " is not an instance of " + eventClass + ".");
		if(event.type != eventType) throw new ArgumentError("Event object has incorrect type. Expected <" + eventType + "> but was <" + event.type + ">.");
		return target.dispatchEvent(event);
	}

	function registerListenerWithPriority(listener : Dynamic, once : Bool = false, priority : Int = 0) : ISlot {
		if(target == null) throw new ArgumentError("Target object cannot be null.");
		if(registrationPossible(listener, once))  {
			var slot : ISlot = new Slot(listener, this, once, priority);
			// Not necessary to insertWithPriority() because the target takes care of ordering.
			slots = slots.prepend(slot);
			_target.addEventListener(_eventType, slot.execute1, false, priority);
			return slot;
		}
		return slots.find(listener);
	}

	function registrationPossible(listener : Dynamic, once : Bool) : Bool {
		if(!slots.nonEmpty) return true;
		var existingSlot : ISlot = slots.find(listener);
		if(existingSlot != null)  {
			if(existingSlot.once != once)  {
				// If the listener was previously added, definitely don't add it again.
				// But throw an exception if their once value differs.
				throw new IllegalOperationError("You cannot addOnce() then add() the same listener without removing the relationship first.");
			}
			return false;
		}
		return true;
	}

}

